#!/usr/bin/perl
# cybozu2ical: Convert Cybozu Office 6 calendar into iCal format
#
# $Id$

use strict;
use lib 'lib';

use YAML;
use Encode qw/decode_utf8 encode/;
use WWW::CybozuOffice6::Calendar;
use Data::ICal;
use Data::ICal::Entry::Event;
use Data::ICal::Entry::TimeZone;
use Data::ICal::Entry::TimeZone::Standard;
use DateTime;
use Pod::Usage;
use Getopt::Long;

our $VERSION = '0.13';

### TRICK (stop escaping for 'exdate' property)
*Data::ICal::Property::_value_as_string = sub {
    my $self = shift;
    my $key = shift;
    my $value = defined($self->value()) ? $self->value() : '';
    
    unless ($self->vcal10) {
        my $lc_key = lc($key);
        $value =~ s/\\/\\/gs;
        $value =~ s/\Q;/\\;/gs unless ($lc_key eq 'rrule' || $lc_key eq 'exdate');
        $value =~ s/,/\\,/gs unless ($lc_key eq 'rrule' || $lc_key eq 'exdate');
        $value =~ s/\n/\\n/gs;
        $value =~ s/\\N/\\N/gs;
    }

    return $value;

};
### END

my %opt = (conf => 'config.yaml');
GetOptions(\%opt, 'help', 'debug', 'conf=s') or pod2usage(2);
pod2usage(1) if $opt{help};

my $cfg = YAML::LoadFile($opt{conf});

my $time_zone = $cfg->{time_zone} || 'Asia/Tokyo';

my $vcalendar = Data::ICal->new();
$vcalendar->add_properties(
    prodid   => "-//as-is.net/Cybozu2ICal $VERSION//EN",
    calscale => 'GREGORIAN',
    method   => 'PUBLISH',
    $cfg->{calname} ? ('X-WR-CALNAME' => $cfg->{calname}) : (),
    'X-WR-TIMEZONE' => $time_zone
);

my $cal = WWW::CybozuOffice6::Calendar->new(%$cfg);
for my $item ($cal->get_items()) {
    my $vevent = Data::ICal::Entry::Event->new();
    my %args = (
	summary     => decode_utf8($item->summary),
	description => decode_utf8($item->description),
	created     => to_icaldate($item->created),
	dtstamp     => to_icaldate($item->modified),
    );

    $args{comment} = decode_utf8($item->comment)
	if $opt{debug} && $item->comment;

    if ($item->is_full_day) {
	$args{dtstart} = [ to_icaldate($item->start, 1), { VALUE => 'DATE' } ];
	$args{dtend}   = [ to_icaldate($item->end,   1), { VALUE => 'DATE' } ];
    } else {
	$args{dtstart} = [ to_icaldate($item->start, 0), { TZID => $time_zone } ];
	$args{dtend}   = [ to_icaldate($item->end,   0), { TZID => $time_zone } ];
    }

    # handle frequency
    if ($item->can('frequency')) {
	# rrule
	my $freq = $item->frequency;
	my %rrules = $freq ne 'WEEKDAYS' ?
	    ( FREQ => $freq, WKST => 'SU' ) :
	    ( FREQ => 'WEEKLY', WKST => 'SU', BYDAY => 'MO,TU,WE,TH,FR' );
	$rrules{UNTIL} = to_icaldate($item->until, $item->is_full_day)
	    if $item->until;
	$args{rrule} = join ';', map { $_ . '=' . $rrules{$_} } keys %rrules;

	# exdate
	if ($item->exdates) {
	    my $exdate;
	    if ($item->is_full_day) {
		$exdate = join ',', map { to_icaldate($_, 1) } $item->exdates;
		$args{exdate} = [ $exdate, { VALUE => 'DATE' } ];
	    } else {
		$exdate = join ',', map { to_icaldate($_, 0) } $item->exdates;
		$args{exdate} = [ $exdate, { TZID => $time_zone } ];
	    }
	}

    }

    # set uid (recommended to be the identical syntax to RFC822)
    $args{uid} = $args{created} . '-' . $item->id . '@' . $cal->host;

    $vevent->add_properties(%args);
    $vcalendar->add_entry($vevent);
}

my $vtimezone = Data::ICal::Entry::TimeZone->new();
$vtimezone->add_properties(tzid => $time_zone);

# probably we need to support the Daylight Saving Time
my $standard = Data::ICal::Entry::TimeZone::Standard->new();

my $std = DateTime->new(year => 1970, month => 1, day => 1,
			hour => 0, minute => 0, second => 0,
			time_zone => $time_zone);
my $offset = DateTime::TimeZone::offset_as_string($std->offset) || '+0900';
my $tzname = $cfg->{tzname} || 'JST';

$standard->add_properties(
    tzoffsetfrom => $offset,
    tzoffsetto   => $offset,
    tzname       => $tzname,
    dtstart      => to_icaldate($std)
);
$vtimezone->add_entry($standard);

$vcalendar->add_entry($vtimezone);

print encode_($cfg->{output_encoding} || 'utf8', $vcalendar->as_string);

sub to_icaldate {
    my($dt, $is_full_day) = @_;
    $is_full_day ?
	$dt->ymd('') :
	$dt->ymd('') . 'T' . $dt->hms('') . ($dt->time_zone->is_utc ? 'Z' : '');
}

sub encode_ {
    my($enc, $text) = @_;
    if ($enc eq 'ncr') {
	$text =~ s/(\P{ASCII})/sprintf("&#%d;", ord($1))/eg;
    } else {
	$text = encode($enc, $text);
    }
    $text;
}

1;
__END__

=head1 NAME

cybozu2ical - Convert Cybozu Office 6 calendar into iCal format

=head1 SYNOPSIS

  % cybozu2ical
  % cybozu2ical --conf /path/to/config.yaml

=head1 DESCRIPTION

C<cybozu2ical> is a command line application that fetches Cybozu
Office 6 calendar items and converts them into a iCal file.  It allows
you to easily integrate the Cybozu Calendar into iCalendar-enabled
Calendar applications, such as Microsoft Outlook, Apple iCal, and of
course, Google Calendar.

You can run this via crontab, for example, every 1 hour.

=head1 REQUIREMENT

This application requires perl 5.8.0 with following Perl modules
installed on your box.

=over 4

=item WWW::CybozuOffice6::Calendar

=item Text::CSV_XS

=item Data::ICal

=item DateTime

=item YAML

=back

=head1 OPTIONS

This application has a command-line option as follows:

=over 4

=item --conf /path/to/config.yaml

Specifies the path to a configuration file. By default, C<config.yaml>
in the current directory.

=back

=head1 CONFIGURATION

The distributions includes a sample configuration file
C<config.yaml.sample>. You can rename it to C<config.yaml> and
configure C<cybozu2ical>.

=over 4

=item cybozu_url

Set the URL of your Cybozu Office 6.

=item username, userid

Set your username or userid for Cybozu Office 6.

=item password

Set your password for Cybozu Office 6.

=item time_zone

Set the timezone of your Cybozu Office 6 (e.g., Asia/Tokyo).

=item tzname

Set the short timezone name of your Cybozu Office 6 (e.g., JST).

=back

=head1 DEVELOPMENT

The development version is always available from the following
subversion repository:

  http://code.as-is.net/svn/public/cybozu2ical/trunk/

You can browse the files via Trac from the following:

  http://code.as-is.net/public/browser/cybozu2ical/trunk/

Any comments, suggestions, or patches are welcome.

=head1 AUTHOR

Hirotaka Ogawa E<lt>hirotaka.ogawa at gmail.comE<gt>

This script is free software and licensed under the same terms as Perl
(Artistic/GPL).

=cut
